#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''This module makes it easy for you to implement a generic /etc/init.d script
(to start and stop a service). It was written to start a CherryPy webserver
at boot time, but should be able to help you with any service, really.

Currently it uses a couple of Ubuntu/Debian specific tools. More specifically,
we are using "start-stop-daemon", a Debian program that can be reused
to implement services.

If you use another distribution, help us make this more general by not
necessarily using these tools.

We don't intend to support Windows.

The great advantage I see in using this is that you define things
Once and Only Once.

The responsibilities of a init.d script are described here:
http://www.debian.org/doc/debian-policy/ch-opersys.html
See section 9.3.2.

May be useful in the future:
http://benrobb.com/2007/01/15/howto-start-subversion-at-boot-on-ubuntu/
http://homepage.hispeed.ch/py430/python/daemon.py
http://homepage.hispeed.ch/py430/python/daemon
http://antonym.org/2005/12/dropping-privileges-in-python.html
http://snipplr.com/view/5503/drop-privileges/
http://articles.slicehost.com/2007/10/17/ubuntu-lts-adding-an-nginx-init-script

In short, you will write a short script that imports this module and uses the
Daemon class. Your script will be started by the system, as root,
with one argument, typically "start" or "stop". It can also be called by a
system administrator, so we have implemented the "restart" method.
If you like, you can subclass Daemon to define "reload" and "force_reload".

EXAMPLE
=======

Suppose we want to run a CherryPy webserver when the computer boots up.
We already have a Python script that starts it up, which we have used
in development. Let's say it is called start_cherrypy.py. Here is what we do:

    cd /etc/init.d/     # Go to the directory where daemons reside
    touch my_website    # Create a new Python program called my_website
    chmod +x my_website # Make it executable
    nano my_website     # Write the program

The contents of this program are typically:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import sys
    from webpyte.daemon import Daemon
    daemon = Daemon(name='my_website',
                directory='/path/to/web/server',
                command='start_cherrypy.py',
                command_args=['prod'],
                user='nando',    # user under which the daemon must run
                group='nando',   # or 'users',
                background=True, # for programs that don't detach on their own
                pidfile='my_website.pid')  # pidfile will be in /var/run/
    daemon(sys.argv[1])       # argv[1] will be 'start', 'stop', 'restart'...

Save it, then test it like this:

    sudo ./my_website start
    sudo ./my_website restart
    sudo ./my_website reload  # won't work unless you write a reload() method
    sudo ./my_website stop

The "stop" command sends the TERM signal to the daemon and waits 30 seconds.
If still running, it sends the KILL signal and waits 5 seconds.
These timeouts can be configured in an argument to the Daemon constructor.

If it is all working fine, it is time to run update-rc.d in order to
create the symlinks in the appropriate runlevels. Example:

    update-rc.d my_website defaults 80 20

And you're finished.

TODO: http://wiki.debian.org/LSBInitScripts

What if my imports don't work?
==============================

A common problem is that libraries aren't found. The reason can be:

1) Because the "my_website" daemon is started as root and some of the libraries
aren't installed system-wide, they are only available for one user.

2) Because the bash environment, including the PYTHONPATH variable, is not
loaded before the "start_cherrypy.py" program is started.

As you can see, this issue affects both programs. This problem is complicated
because I use setuptools to easy_install the libraries, and they end up
in subdirectories of "egg-directories" of a single libraries directory, e.g.:
/path/to/python/libraries/cherrypy.egg.some.version/cherrypy/

I mean, the libraries are even hard to find because they are not just
subdirectories of a single directory.

I spent a long time on this problem and finally found the following
single solution. Near the top of both Python scripts, add something like this:

    import site, os
    site.addsitedir(os.path.expanduser('~/path/to/your/python/libraries'))

The site.addsitedir() function will read the .pth file that setuptools
creates on your lib directory and will make all listed libraries available
by adding their directories to sys.path.
'''

from __future__ import absolute_import
# http://docs.python.org/whatsnew/pep-328.html
# We require Python 2.6:
from __future__ import print_function   # deletes the print statement
from __future__ import unicode_literals # unicode by default

__author__    = "Nando Florestan"
__version__   = "0.01"
__copyright__ = "2009-07-19"
__license__   = 'public domain'

import os
import sys
from subprocess import PIPE, call


class Path(object):
    def __init__(self, *args):
        if len(args) == 1:
            self.path = args[0]
        else:
            self.path = os.path.join(*args)
    
    def is_executable(self):
        return os.access(self.path, os.X_OK)
    
    def __unicode__(self):
        return unicode(self.path)



class Daemon(object):
    '''Represents a system daemon with all its configuration.
    Has a default implementation of the start, stop and restart commands.
    Subclass Daemon if you wish to provide reload() and force_reload().
    '''
    def __init__(self, name, directory, command, user, group, command_args=None,
                 background=True, pidfile=None, stopkill=(30, 5)):
        self.name = name
        self.user = user
        self.group = group
        self.background = background
        self.stopkill = stopkill
        self.directory = directory
        self.command = command
        self.command_path = Path(directory, command)
        self.command_args = [command_args] \
            if isinstance(command_args, basestring) else command_args
        if pidfile:
            self.pidfile = pidfile
        else:
            self.pidfile = name.replace(' ', '_') + '.pid'
        # Ensure pidfile is rooted
        if not self.pidfile.startswith('/'):
            self.pidfile = '/var/run/' + self.pidfile
    
    def __call__(self, command):
        if command == 'start':
            retcode = self.start()
        elif command == 'stop':
            retcode = self.stop()
        elif command == 'restart':
            retcode = self.restart()
        elif command == 'force-reload':
            retcode = self.force_reload()
        elif command == 'reload': # This is the only optional command
            retcode = self.reload()
        else:
            raise RuntimeError(b'Unknown command: ' + command)
        sys.exit(retcode)
    
    def ssd_args(self):
        return ['start-stop-daemon',
                '--oknodo', # returns 0 even if nothing is done
                '--pidfile', self.pidfile,
                '--chuid', self.user,
                '--user', self.user,
                '--group', self.group,
                '--chdir', self.directory,
                #'--startas', self.command,
               ]
    
    def start_args(self):
        args = self.ssd_args()
        args.extend([
            '--start',
            '--make-pidfile',
            '--name', self.name,
            '--startas', self.command,
        ])
        if self.background:
            args.append('--background')
        if self.command_args:
            args.append('--')
            args.extend(self.command_args)
        return args
    
    def start(self):
        #print(self.start_args())
        return self.go(self.start_args())
    
    def go(self, args):
        #args = b' '.join(args)
        return call(args=args, shell=False) #, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    
    def stop_args(self):
        args = self.ssd_args()
        args.append('--stop')
        args.append('--retry')
        args.append('TERM/{0}/KILL/{1}' \
            .format(self.stopkill[0], self.stopkill[1]))
        return args
    
    def stop(self):
        return self.go(self.stop_args())
    
    def restart(self):
        retcode = self.stop()
        if retcode != 0:
            return retcode
        # import time; time.sleep(1)
        return self.start()


# TODO: Implement a "install" command that copies this to /etc/init.d and
# runs update-rc.d to create the symlinks in the appropiate runlevels.
